import * as THREE from "three"
import Module from "./Module"
import MattertagsData from "../data/MattertagsData"
import { GetDiscPositionsCommand } from "../command/mattertag.command"
import CameraData from "../data/CameraData"
import { MattertagViewedMessage, MattertagNavigatedMessage } from "../message/mattertag.message"
import { BeginSwitchViewmodeMessage } from "../message/viewmode.message"
import { ApplicationChangeMessage } from "../message/app.message"
import transitionTypeEnum from "../enum/transitionType.enum"
import SweepData from "../data/SweepData"
import SettingsData from "../data/SettingsData"
import { NavigateToMattertagCommand, MattertagNavigateToLinkCommand } from "../command/mattertag.command"
import { FocusOnPinInsideCommand, NavigateToSweepCommand } from "../command/navigate.command"
import Logger from "../utils/Logger"
import { ChangeViewmodeCommand } from "../command/viewmode.command"
import MattertagsViewData from "../data/MattertagsViewData"
import Vectors from "../three/Vectors"
import PinsModule from "./PinsModule"
import { PinClickedMessage, PinHoverChangeMessage } from "../message/pin.message"
import mattertagEnum from "../enum/mattertag.enum"
import { appTypeMin } from "../enum/appType.enum"
import BillboardData from "../object/BillboardData"
import modeEnum from "../enum/mode.enum"
import pinTypeEnum from "../enum/pinType.enum"
import pinColorEnum from "../enum/pinColor.enum"
import { BeginMoveToSweepMessage } from "../message/sweeps.messge"
import keyConst from "../constant/key.const"
import { isPanOrMesh } from "../utils/viewmode.util"
import EngineContext from "../EngineContext"
const D = new Logger("mattertag-viewdata-module")
export default class MattertagViewDataModule extends Module {
  distance: THREE.Vector3
  cameraPosition: THREE.Vector3
  hoverTimeout: number
  updateViewData: () => void
  data: MattertagsData
  viewData: MattertagsViewData
  onTagDirectionChanged: any
  onCameraUpdate: (e: any) => void
  updateBillboardData: (e: any, t: any) => void
  addActiveBillboard: (e: any) => void
  removeFromActiveBillboards: (e: any) => void
  handleOpenBillboardChanged: () => void
  engine: EngineContext
  onSweepChosen: (e: any) => void
  onDiscHoverChange: (e: any) => void
  settings: SettingsData
  onDiscClicked: (e: any) => void
  onSweepExit: (e: any) => void
  onAppChange: (e: any) => void
  currentApp: any
  onViewmodeChange: (e: any) => void
  onNavigationLinkClicked: (e: any) => Promise<void>
  sweepData: SweepData
  cameraData: CameraData
  pinRenderer: PinsModule["pinRenderer"]
  constructor() {
    super(...arguments)
    this.name = "mattertags-viewdata"
    this.distance = new THREE.Vector3()
    this.cameraPosition = new THREE.Vector3()
    this.hoverTimeout = 0
    this.updateViewData = () => {
      this.data.iterate(e => {
        this.viewData.derivedData[e.sid] = this.makeViewDataForMattertag(e)
      })
      this.viewData.commit()
    }
    this.makeViewDataForMattertag = e => {
      const t = this.viewData.activeBillboards.get(e.sid),
        i = this.viewData.selectedTag === e.sid,
        n = t ? t.position : null,
        s = t ? t.distance : 0
      return {
        position: n,
        selected: i,
        located: !(!n || !s),
        closeTheTag: !t || t.closing || !(i || !this.viewData.selectedTag),
        navigating: !(!this.viewData.navigating || this.viewData.navigating !== e.sid),
        navigatingPosition: i ? this.viewData.getNavigatingPosition() : void 0,
        positioned: !1,
        distance: s,
        stemHeight: e.stemHeight,
        visible: e.enabled,
        color: `#${e.color.getHexString()}`,
        onTagDirectionChanged: this.onTagDirectionChanged
      }
    }
    this.onCameraUpdate = e => {
      this.viewData.activeBillboards.keys.length > 0 &&
        (this.cameraPosition.copy(e.pose.position), this.updateActiveBillboards(this.viewData.activeBillboards.keys), this.viewData.commit())
    }
    this.updateBillboardData = (e, t) => {
      const i = this.viewData.activeBillboards.get(e)
      i && (Object.assign(i, t), i.commit())
    }
    this.addActiveBillboard = e => {
      this.viewData.activeBillboards.has(e) || this.viewData.activeBillboards.set(e, new BillboardData(e))
    }
    this.removeFromActiveBillboards = e => {
      this.viewData.activeBillboards.has(e) && this.viewData.activeBillboards.delete(e)
    }
    this.handleOpenBillboardChanged = () => {
      const e = this.viewData.openTag
      this.viewData.activeBillboards.atomic(() => {
        e &&
          (this.viewData.activeBillboards.get(e)
            ? (window.clearTimeout(this.viewData.activeBillboards.get(e).timeoutId),
              this.updateBillboardData(e, {
                timeoutId: 0,
                closing: !1
              }))
            : this.addActiveBillboard(e),
          this.updateActiveBillboards([e]))
        for (const t of this.viewData.activeBillboards.keys) {
          if (t === e) return
          this.updateBillboardData(t, {
            closing: !0,
            direction: void 0,
            timeoutId: window.setTimeout(() => {
              this.removeFromActiveBillboards(t)
            }, 1000)
          })
        }
      })
    }
    this.updateActiveBillboards = async e => {
      try {
        await this.engine.commandBinder.issueCommand(new GetDiscPositionsCommand(e)).then(e => {
          for (const t of e)
            this.updateBillboardData(t.sid, {
              distance: this.distance.copy(this.cameraPosition).distanceToSquared(t.world),
              position: t.screen
            })
        })
      } catch (e) {
        D.error(`Could not get Disc Positions for tags - ${e}`)
      }
    }
    this.onSweepChosen = e => {
      this.viewData.setNavigatingPosition(e)
    }
    this.onDiscHoverChange = e => {
      if (this.settings.tryGetProperty(keyConst.rule.features.tag, !1)) return
      if (e.pinType !== pinTypeEnum.MATTERTAG) return
      const t = this.viewData.toolState
      if (t !== mattertagEnum.CLOSED && this.viewData.selectedTag) return
      const i = this.data.getTag(e.id)
      if (i && i.enabled) {
        clearTimeout(this.hoverTimeout)
        if (e.hovering) t !== mattertagEnum.EDITING && (this.hoverTimeout = window.setTimeout(() => this.discHoverChange(e.id, e.hovering), 125))
        else {
          if (e.id === this.viewData.selectedTag) return
          this.discHoverChange(e.id, !1)
        }
      }
    }
    this.onDiscClicked = e => {
      if (this.settings.tryGetProperty(keyConst.rule.features.tag, !1)) return
      const t = this.viewData.toolState
      e.pinType === pinTypeEnum.MATTERTAG &&
        t === mattertagEnum.CLOSED &&
        (e.id === this.viewData.selectedTag
          ? this.closeBillboard(e.id)
          : (this.viewData.setSelectedTag(e.id), this.navigateToTag(e.id, transitionTypeEnum.Interpolate)))
    }
    this.onSweepExit = e => {
      e.toSweep && e.fromSweep !== e.toSweep && (this.viewData.isCreatingNewMattertag() || this.viewData.closeOpenBillboard())
    }
    this.onAppChange = e => {
      this.currentApp = e.application
      this.viewData.closeOpenBillboard()
      this.viewData.setSelectedTag(null)
    }
    this.onViewmodeChange = e => {
      null === this.viewData.navigating && isPanOrMesh(e.fromMode) && this.viewData.closeOpenBillboard()
    }
    this.onNavigationLinkClicked = async e => {
      if (e.chunk.link && e.chunk.link.navigationData) {
        const t = { 2: modeEnum.DOLLHOUSE, 3: modeEnum.FLOORPLAN },
          { sweepIndex: i, quaternion: n, mode: s } = e.chunk.link.navigationData
        let { panoId: r, position: o } = e.chunk.link.navigationData
        if (!r && void 0 !== i) {
          const e = this.sweepData.getSweepByIndex(i)
          e && (r = e.id)
        }
        if (r)
          this.engine.commandBinder.issueCommand(
            new NavigateToSweepCommand({
              sweep: r,
              rotation: n,
              transition: transitionTypeEnum.FadeToBlack
            })
          )
        else {
          if (!(s in t)) throw new Error("Unknown navigation link pose: " + JSON.stringify(e.chunk.link.navigationData))
          this.engine.commandBinder.issueCommand(
            new ChangeViewmodeCommand(t[s], transitionTypeEnum.Interpolate, {
              rotation: n,
              position: o
            })
          )
        }
      }
    }
    this.onTagDirectionChanged = (e, t) => {
      this.updateBillboardData(e, { direction: t })
    }
  }
  makeViewDataForMattertag: any
  updateActiveBillboards: any
  async init(e, t: EngineContext) {
    this.engine = t
    const [i, n, s, o, l] = await Promise.all([
      t.market.waitForData(MattertagsData),
      t.market.waitForData(SweepData),
      t.market.waitForData(SettingsData),
      t.market.waitForData(CameraData),
      t.getModule(PinsModule)
    ])
    this.data = i
    this.sweepData = n
    this.settings = s
    this.cameraData = o
    this.pinRenderer = l.pinRenderer
    this.viewData = t.market.tryGetData(MattertagsViewData) || new MattertagsViewData(this.data)
    t.market.register(this, MattertagsViewData, this.viewData)
    this.viewData.setViewDataFactory(this.makeViewDataForMattertag)
    this.updateViewData()
    this.settings.onPropertyChanged(keyConst.rule.features.mattertags, e => {
      !!e && this.viewData.closeOpenBillboard()
    })
    this.bindings.push(
      t.subscribe(PinHoverChangeMessage, this.onDiscHoverChange),
      t.subscribe(PinClickedMessage, this.onDiscClicked),
      t.subscribe(BeginMoveToSweepMessage, this.onSweepExit),
      t.subscribe(BeginSwitchViewmodeMessage, this.onViewmodeChange),
      t.subscribe(ApplicationChangeMessage, this.onAppChange),
      this.cameraData.onChanged(this.onCameraUpdate),
      this.data.onChanged(this.updateViewData),
      this.viewData.onOpenTagChanged(this.handleOpenBillboardChanged),
      this.viewData.activeBillboards.onChanged(this.updateViewData)
    )
    this.engine.commandBinder.addBinding(MattertagNavigateToLinkCommand, this.onNavigationLinkClicked)
    this.engine.commandBinder.addBinding(NavigateToMattertagCommand, async e => this.navigateToTag(e.sid, e.transition, e.navigateToDisabled))
  }
  openBillboard(e) {
    const t = this.viewData.tagCapabilities[e]
    ;(t && !t.opening) || this.viewData.openBillboard(e)
  }
  closeBillboard(e) {
    this.viewData.closeBillboard(e)
  }
  async navigateToTag(e: string, t: number, i = !1) {
    if (this.settings.tryGetProperty(keyConst.rule.features.tag, !1)) return
    const n = this.data.getTag(e)
    if (!n) throw new Error("No tag with that sid was found")
    if (!n.enabled && !i) throw new Error("Cannot navigate to a disabled mattertag")
    const s = this.viewData.tagCapabilities[e]
    if (s && !s.navigating) return
    this.viewData.setNavigatingTag(e)
    const r = this.viewData.activeBillboards.get(e)
    const o = r && r.direction
    const { anchorPosition: a, stemVector: l, stemHeight: c } = n
    const h = this.tagDirectionToOrientationAdjust(o)
    const d = { anchorPosition: a, stemNormal: l, stemLength: c }
    return this.engine.commandBinder
      .issueCommand(
        new FocusOnPinInsideCommand({
          pinPosition: d,
          transition: t,
          orientationAdjust: h,
          onSweepChosen: this.onSweepChosen
        })
      )
      .then(() => {
        this.viewData.setSelectedTag(e)
        this.onTagVisited(e)
        this.openBillboard(e)
        this.viewData.setNavigatingTag(null)
        this.viewData.setNavigatingPosition(void 0)
      })
      .catch(e => {
        D.debug(e), this.viewData.setNavigatingTag(null), this.viewData.setNavigatingPosition(void 0)
      })
  }
  onTagVisited(e: string) {
    this.viewData.visitTag(e), this.engine.broadcast(new MattertagViewedMessage(e, "open")), this.engine.broadcast(new MattertagNavigatedMessage(e))
  }
  discHoverChange(e, t) {
    if (this.settings.tryGetProperty(keyConst.rule.features.tag, !1)) return
    if (!this.data.getTag(e)) return
    const i = this.viewData.openTag === e
    t && !i
      ? (this.currentApp === appTypeMin.SHOWCASE && this.engine.broadcast(new MattertagViewedMessage(e, "hover")),
        this.openBillboard(e),
        this.pinRenderer.setPinColorVariant(e, pinColorEnum.HIGHLIGHTED))
      : !t && i && (this.closeBillboard(e), this.pinRenderer.setPinColorVariant(e, pinColorEnum.DEFAULT)),
      this.viewData.commit()
  }
  tagDirectionToOrientationAdjust(e: string, t = new THREE.Quaternion()) {
    const i = 0.25
    let s = 0
    let r = 0
    if (e) {
      switch (e) {
        case mattertagEnum.RIGHT:
        case mattertagEnum.RIGHT_DOWN:
        case mattertagEnum.RIGHT_UP:
          s = -0.25
          break
        case mattertagEnum.LEFT:
        case mattertagEnum.LEFT_DOWN:
        case mattertagEnum.LEFT_UP:
          s = i
      }
      switch (e) {
        case mattertagEnum.UP:
        case mattertagEnum.LEFT_UP:
        case mattertagEnum.RIGHT_UP:
          r = i
          break
        case mattertagEnum.DOWN:
        case mattertagEnum.LEFT_DOWN:
        case mattertagEnum.RIGHT_DOWN:
          s = -0.25
      }
    } else (s = -0.25), (r = -0.25)
    t.setFromAxisAngle(Vectors.RIGHT, r)
    t.multiply(new THREE.Quaternion().setFromAxisAngle(Vectors.UP, s))
    return t
  }
}
